본문으로 건너뛰기

React Hooks

그런데 막상 실무에서 보면 useState, useEffect는 쓰고 있지만
왜 이렇게 써야 하는지는 애매하게 알고 넘어가는 경우가 많다.

이 글에서는 React 19 기준으로
Hooks를 API 목록이 아니라 렌더링 규칙 위에서 동작하는 도구로 다시 정리해보려고 한다.


Hooks를 이해하기 전에 꼭 받아들여야 할 전제

  • 컴포넌트는 객체가 아니라 함수다.
  • 렌더링은 한 번 실행되고 끝나는 게 아니라, 계속 다시 실행되는 계산이다.
  • Hooks는 렌더링 과정에 끼어드는 장치다.
function Counter() {
const [count, setCount] = useState(0);
return <button>{count}</button>;
}

이 함수는 한 번 만들어지고 유지되는 게 아니다.
렌더링이 일어날 때마다 처음부터 다시 실행된다.


useState

상태는 저장이 아니라 “다음 렌더링을 위한 값”

const [count, setCount] = useState(0);

useState를 쓰면 흔히 이렇게 생각한다.

“count라는 값을 메모리에 저장해두는 거구나”

하지만 실제로는 조금 다르다.

상태 업데이트는 즉시 반영되지 않는다.

setCount(count + 1);
console.log(count); // 여전히 이전 값

setCount는 값을 바로 바꾸지 않는다.
다음 렌더링에서 사용할 값을 예약할 뿐이다.

그래서 연속 업데이트에서는 함수형 업데이트가 필요하다.

setCount((prev) => prev + 1);
setCount((prev) => prev + 1);

이 패턴은 권장사항이 아니라 동시성 렌더링 환경에서 안전하게 동작하기 위한 필수 패턴이다.


useEffect

렌더링이 끝난 “뒤”에 실행되는 코드

useEffect(() => {
document.title = `count: ${count}`;
}, [count]);

useEffect는 렌더링을 구성하지 않는다.
이미 그려진 결과에 반응할 뿐이다.

그래서 다음과 같은 코드는 항상 위험하다.

useEffect(() => {
setCount(count + 1);
}, [count]);

→ 렌더링 → effect → 상태 변경 → 렌더링
무한 루프의 전형적인 형태다.

Effect는 “상태를 만들기 위한 수단”이 아니라
“렌더링 결과와 외부 세계를 동기화하는 장치”다.


useLayoutEffect

화면이 그려지기 직전에 개입해야 할 때

useLayoutEffect(() => {
const height = ref.current.offsetHeight;
setHeight(height);
}, []);

useLayoutEffect는 DOM이 바뀐 직후, 브라우저가 화면을 그리기 전에 실행된다.

그래서:

  • 레이아웃 측정
  • 스크롤 위치 보정
  • 깜빡임 방지

같은 경우에만 사용해야 한다.

남용하면 렌더링 전체를 막아버린다.


useRef

렌더링과 무관한 값을 들고 있을 때

const inputRef = useRef(null);

useRef의 핵심 특징은 이것이다.

  • 값이 바뀌어도 렌더링을 트리거하지 않는다
  • 렌더링 사이에서 값을 유지한다
const renderCount = useRef(0);
renderCount.current += 1;

이 패턴은 디버깅이나 외부 라이브러리 연동에서 자주 쓰인다.


useMemo

값을 캐싱하는 게 아니라 “계산을 생략”하는 도구

const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);

useMemo는 값을 저장하려고 쓰는 게 아니다.

  • 의존성이 바뀌지 않으면
  • 계산 자체를 다시 하지 않는다

계산 비용이 크지 않다면, 쓰지 않는 게 더 낫다.


useCallback

const onClick = useCallback(() => {
setCount((c) => c + 1);
}, []);

useCallback은 함수 재생성을 막기 위한 게 아니다.
참조 동일성을 유지하기 위한 도구다.

주 용도는:

  • React.memo와 함께 사용
  • deps 배열에 함수가 들어가는 경우

함수 메모이제이션의 진짜 목적이다.


React 19: useTransition

사용자 경험을 지키는 Hook

const [isPending, startTransition] = useTransition();

startTransition(() => {
setList(nextList);
});

이 업데이트는 “중요하지만 급하지 않은 작업”으로 처리된다.

  • 입력은 즉시 반응
  • 리스트 업데이트는 뒤에서 처리

React 19: useActionState

서버 액션과 상태의 연결

const [state, action, isPending] = useActionState(createPost, initialState);

이 Hook은 서버 액션을 호출하면서 생기는

  • 로딩
  • 에러
  • 결과

를 렌더링 흐름 안으로 자연스럽게 끌어온다.


Custom Hook

로직을 숨기는 게 아니라 드러내는 방식

function useCounter() {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
return { count, increase };
}

Custom Hook의 목적은:

  • 중복 제거
  • 관심사 분리
  • 의도 표현

정리

Hooks는 편의 기능이 아니다.

  • 렌더링은 계산이다
  • 상태는 다음 렌더링을 위한 입력이다
  • Hooks는 이 규칙을 코드로 강제한다